今天大概會聊到的範圍
@Composable
- compose compiler & runtime
爆個雷:今天的文章只會講前半段,還不會回答 為什麼 remember 是 composable function?
今天第 15 天,剛好走到鐵人賽的一半,也算認真看 Compose 這個主題好一段時間了。但我一直有個疑惑:到底 @Composable
是什麼東西?
這個問題聽起來可能很蠢,不就是我們在寫 Compose UI 時,那些 UI element 都要加上 Composable ,所以 @Composable
應該是標示某一個 function 將會被視為 View 對吧?
一開始我也是這樣想,直到我看到 remember 的定義:
@Composable
inline fun <T> remember(calculation: @DisallowComposableCalls () -> T): T =
currentComposer.cache(false, calculation)
等等!remember
為什麼是一個 composable function? 他不是 UI element 啊?
也許,remember
會操控到 UI 的內容勉強還說得過去。但是另一個例子:當我們需要 dp/sp/px 大小單位轉換時,我們需要 LocalDenesity.current
,但 LocalDensity.current
的 getter 長這個樣子
inline val current: T
@ReadOnlyComposable
@Composable
get() = currentComposer.consume(this)
為什麼你也是 composable function ???
@Composable
這個 Annotation 做了什麼簡單來說,@Compoable
這個 annotation 改變了這個 function 的性質。有一個很好的比喻的就是 suspend function ,加上 suspend
關鍵字的 function 會有與一般不同的行為,suspend
function 只能在特定的 scope 或是別的 suspend function 中呼叫。@Composable
也是,加上 @Composable
這個 annotation 的 function 會有不同的行為,而且 @Composable
只能在別的 @Composable
function 中呼叫。
@Composable
fun composableFunc()
@Composable
fun anotherCompFunc() {
composableFunc() // ok
}
fun normalFunc(){
composableFunc() // not allow
}
在我們用到 compose 相關的功能時,我們的專案都會執行 compose compiler。compose compiler 基本上是一個 gradle plugin 在 compile time 加入我們專案的建置。這個 plugin 會找到所有有標上
@Composable
annotation 的東西 source 並且檢查這些 composable 是否是在別的 composable function 中呼叫的 source 。
@Composalbe
並不會觸發 annotation processor,而是可以視之為一個 keyword,就如同上面舉例的 suspend 一樣。他的目的是讓 compiler 知道這個是 composable function,需要特別判斷與處理
既然 composable function 一定要在別的 composable function 內才能呼叫,那就會有一個雞生蛋蛋生雞的問題:第一個 composable function 要在哪裡呼叫?
就和 suspend
function 需要再 coroutine scope 中呼叫一樣,composable function 也須要有一個 entry point。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeApp()
}
}
}
ComponentActivity
的 setContent
function 就是一個 entry point。這個 function 本身不是 composable 但卻接收了一個 composable function,因此,這個 function 內部並沒有直接“呼叫”這個 content ( 這個 composable function ) 而是將它繼續往下傳遞。經過很多層包裝後,最終會
- 傳遞鏈大概是:
START: ComponentActivity.setContent
=> ComposeView.setContent
=> ViewGroup.setContent
=> WrappedComposition.setContent- 在 ViewGroup.setContent 中,會建立一個
Composition
。並將Composition
和 content 都往下傳遞WrappedComposition.setContent
會將 content 放進 Composition 中並執行 (所以實際執行是在Composition
)Composition
中會 new 出一個ComposerImpl
(CompserImpl
是Composer
的實體)Composer
中,最後會將composable
傳遞到invokeCompoable
執行。這個 function 特別將 composable 是為一般 function 執行,成為一般 function 和 composable function 的交界點
internal fun invokeComposable(composer: Composer, composable: @Composable () -> Unit) {
@Suppress("UNCHECKED_CAST")
val realFn = composable as Function2<Composer, Int, Unit>
realFn(composer, 1)
}
以下引用 Leland Richardson 的圖。接下來大部分的的解釋與理來都來自這個文章:https://medium.com/androiddevelopers/under-the-hood-of-jetpack-compose-part-2-of-2-37b2c20c6cdd
找到 Composable function 的初始執行點了,那這個神秘的 Composer
到底做了什麼呢?在 compose 開始時,可以想像在 compose 啟動開始時,Composer
會建立一個很大的空陣列。
當 Composer 走訪各個 Composable 時,會一一將各個 Composable 放入這個陣列。並保留陣列尾端的空位
當今天有某個 Composable 觸發了 recomposition 時,composer 就會從頭走訪整個陣列。
Composer 取得每個位置的 composable 時,可以依照資料做決定:有可能決定完全不動(假設第一個方形完全沒有改變),有可能決定改變其中的某個參數,但是不影響上下 ( ex. Text ) ( 假設圓形需要改變顏色 )。
當今天某個 recomposition 需要觸發 layout 改變增加 child composable 時,composer 會將尾端的空位直接移上來。
從這裡開始,重新將新的 composable 加入陣列中存放。
這樣存放 composable 的好處,所有的行動(加入 composable、改變 composable 資料、刪除 composable )都是固定時間的 (O(1)) 。唯一耗時的是移動空位 (O(n))。但移動空位通常是有重大 UI 改變時才會觸發,而且每次觸發都是一整個區塊在做改變,因此,Compose UI 團隊才會做這個設計和選擇。
今天的問題還是沒有被回答到。"為什麼 remember 是 composable function?"。但在回答這個之前,我們先碰觸到了 Compose 整個 framework 運作核心的冰山一角。明天預計順著這個邏輯,繼續說明當今天有 State 在 composable tree 中時,composer 怎麼存放資料,state 又怎麼影響 composable。
Reference: